import random

__all__ = [ "Spinner" ]

class Spinner(object):
    """A virtual dictionary of key value pairs that keep changing.
    It is really:
    Two lists, one of keys and one of values.
    Two indeces, one for keys and one for values.
        It is the shift method that makes the key values pairs change.
        For example:
        _normal_shift_distance is 4
        After reset()
            _key_index = 0
            _value_index = 0
        After _normal_shift()
            _key_index becomes 0 + 4
            _value_index is still 0
            _keys[4] becomes the key for _values[0]
    See _normal_shift() used in get_value() and get_key().
    The method shift(n) is like normal shift but it allows the caller set the shift distance.

    MORE DETAIL:
        Example:
        In get_value() and get_key() before the call to _normal_shift()
        the _key_index and the _value_index are both incremented 1.
        So where _keys = ['a', 'b', 'c', 'd', 'e', 'a', 'b', 'c', 'd', 'e']
           and _values = ['z', 'y', 'x', 'w', 'v', 'u', 't', 's', 'r', 'q']
        where _normal_shift_distance is 2
        After reset().
            _key_index = 0
            _value_index = 0
        continuing - the first call to get_value(self, 'a') returns 'z'
            and get_value() also increments both _key_index and _value_index
            _key_index = 1
            _value_index = 1
            and get_value() also calls _normal_shift()
            _key_index = 3 (1 + 2) - _keys[_key_index] is 'd'
            _value_index is still 1 - _values[_value_index] = 'y'
        continuing - the second call to get_value(self, 'a'):
            finds the second key 'a' after incrementing the key index by 2
            _values[1 + 2] which is 'y'
            ... (indeces are incremented and shifted)
            the value 'y' is returned
        
    Peek methods to not change the indeces.    
    """
    def __init__(self, keys, values, shift):
        assert len(keys) > 0 and (len(keys) == len(values))
        self._size = len(keys)
        self._keys = keys
        self._values = values
        self._key_index = 0
        self._value_index = 0
        # the normal shift
        self._normal_shift_distance = shift
        if self._normal_shift_distance < 0:
            self._normal_shift_distance *= -1
        if self._normal_shift_distance > self._size:
            self._normal_shift_distance %= self._size

    def reset(self):
        self._key_index = 0
        self._value_index = 0

    def get_value_length(self):
        """gets the number of character(s) in each value
        this is needed for writing and reading
        the first character(s) in the conversion stream
        """
        if self._size > 0:
            if isinstance(self._values[0], str):
                # string values
                return len(self._values[0])
            else:
                # byte values
                return 1
        else:
            return 0

    def peek_value_at(self, index):
        """used in conversion.
        create a random number for the first shift.
        call this method to get the character(s) at that shift.
        make that character(s) the first character(s) in the ouput.
        use the same random number with the shift method of each spinner."""
        if index < 0:
            index *= -1
        if index >= self._size:
            index %= self._size
        return self._values[index]
    
    def peek_value_index(self, v):
        """used in unconversion.
        use get_value_length to find the number of characters.
        get that # of character(s) from the beginning of the stream.
        use this method to get the index of that string of character(s)
        use the index with the shift method of each spinner.
        discard the character(s)"""
        i_value = self._value_index
        if self._values[i_value] == v:
            return i_value
        last_value_index = i_value
        i_value = self._inc_index(i_value)
        while i_value != last_value_index:
            if self._values[i_value] == v:
                return i_value
            i_value = self._inc_index(i_value)
        # value not found
        return None
    
    def get_value(self, key):
        i_key = self._key_index
        i_value = self._value_index
        if self._keys[i_key] == key:
            # inc the indexes for next time
            self._key_index = self._inc_index(i_key)
            self._value_index = self._inc_index(i_value)
            # return the corresponding value
            self._normal_shift();
            return self._values[i_value]
        # next
        last_key_index = i_key
        last_value_index = i_value
        i_key = self._inc_index(i_key)
        i_value = self._inc_index(i_value)
        while i_key != last_key_index:
            if self._keys[i_key] == key:
                # inc the indexes for next time
                self._key_index = self._inc_index(i_key)
                self._value_index = self._inc_index(i_value)
                # return the corresponding value
                self._normal_shift();
                return self._values[i_value]
            # next
            i_key = self._inc_index(i_key)
            i_value = self._inc_index(i_value)
        # not found
        return None
    
    def peek_random_value(self, ignore_key=None):
        """get a random value
        do not spin the spinner"""
        r_num = random.randint(0, self._size -1)
        r_key = self._keys[r_num]
        if ignore_key is not None:
            if r_key == ignore_key:
                r_num += 1
                if r_num == self._size:
                    r_num = 0
        return self._values[r_num]
    
    def peek_random_index_value(self, ignore_key=None):
        """get a random value
        do not spin the spinner"""
        r_num = random.randint(0, self._size -1)
        r_key = self._keys[r_num]
        if ignore_key is not None:
            if r_key == ignore_key:
                r_num += 1
                if r_num == self._size:
                    r_num = 0
        return (r_num, self._values[r_num])
    
    def get_key(self, value):
        i_key = self._key_index
        i_value = self._value_index
        if self._values[i_value] == value:
            # inc the indexes for next time
            self._key_index = self._inc_index(i_key)
            self._value_index = self._inc_index(i_value)
            # return the corresponding value
            self._normal_shift();
            return self._keys[i_key]
        last_key_index = i_key
        last_value_index = i_value
        i_key = self._inc_index(i_key)
        i_value = self._inc_index(i_value)
        while i_key != last_key_index:
            if self._values[i_value] == value:
                # inc the indexes for next time
                self._key_index = self._inc_index(i_key)
                self._value_index = self._inc_index(i_value)
                # return the corresponding value
                self._normal_shift();
                return self._keys[i_key]
            i_key = self._inc_index(i_key)
            i_value = self._inc_index(i_value)
        # not found
        return None

    def shift(self, n):
        if n != 0:
            if n < 0:
                n *= -1
            if n > self._size:
                n %= self._size
            self._key_index += n
            if self._key_index >= self._size:
                self._key_index -= self._size
    
    def _normal_shift(self):
        # shift the key index but not the value index
        self._key_index += self._normal_shift_distance
        if self._key_index >= self._size:
            self._key_index -= self._size

    def _inc_index(self, n):
        n += 1
        if n == self._size:
            n = 0
        return n
